Skip to content

feat(runtime): rebuild ACP execution as plugin adapters - #5224

Draft
Sun-GLiang wants to merge 2 commits into
apache:mainfrom
Sun-GLiang:feat/antigravity-acp-pr2
Draft

Sun-GLiang wants to merge 2 commits into
apache:mainfrom
Sun-GLiang:feat/antigravity-acp-pr2

Conversation

@Sun-GLiang

@Sun-GLiang Sun-GLiang commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR has been rebuilt from the latest main (ea990cab) after #5283 established the generic Plugin-backed Session executor architecture.

  • Add @maka/acp-executor-plugin, a shared ACP Runtime Plugin that exposes ctx.acp.register(ctx, adapter, config) to child Plugin Entries.
  • Add @maka/antigravity-acp-plugin as a thin Antigravity adapter containing only executable/helper validation, launch environment policy, and optional initial model configuration.
  • Route every ACP Agent through feat(runtime): add plugin-backed Session executors #5283's existing PluginExecutorService / PluginExecutorBackend; ACP is not a second backend or Session-routing authority.
  • Extend the generic executor boundary with hosted permission choices and durable text | file_diff tool results.
  • Retain one ACP process and ACP Session per Maka conversation, with cancellation, process-tree cleanup, workspace-contained file callbacks, tool/thought/text projection, and generic ACP config-option application.
  • Persist a conversation continuity marker. After a Host or Plugin restart, an existing external conversation becomes explicitly history-only instead of silently forking into a new ACP Session.

Refs #5103

Architecture

Runtime layers

flowchart LR
  Client["Desktop / CLI / API"]

  subgraph Maka["Maka generic execution authority"]
    Session["Session Manager<br/>executorId"]
    Backend["PluginExecutorBackend<br/>canonical event + interaction bridge"]
    Registry["PluginExecutorService<br/>scoped registry + generation binding"]
  end

  subgraph ACPPlugin["ACP Runtime Plugin"]
    AcpService["ctx.acp<br/>adapter registration"]
    AcpExecutor["AcpExecutor<br/>protocol + process + Session lifecycle"]
  end

  subgraph Adapters["External-Agent adapter plugins"]
    Antigravity["Antigravity adapter<br/>paths + env + model quirks"]
    Future["Future ACP adapter<br/>Cursor / other Agent"]
  end

  Agent["External ACP process"]
  Forms["Hosted Form authority"]
  Transcript["Canonical Session events"]

  Client -->|"create/send with executorId"| Session
  Session --> Backend
  Backend -->|"generation-pinned execute"| Registry
  Registry --> AcpExecutor

  AcpService -->|"wrap adapter as executor"| AcpExecutor
  Antigravity -->|"ctx.acp.register(ctx, adapter, config)"| AcpService
  Future -->|"ctx.acp.register(ctx, adapter, config)"| AcpService
  AcpExecutor <-->|"ACP over stdio"| Agent

  AcpExecutor -->|"PluginExecutorOutputEvent"| Backend
  Backend --> Transcript
  AcpExecutor -->|"requestPermission"| Backend
  Backend <--> Forms
Loading

The key boundary is the generic executor contract. Maka owns routing, binding, canonical persistence, and hosted interactions; the ACP Runtime Plugin owns ACP mechanics; each adapter owns only product-specific launch/configuration differences.

Setup-to-plugin activation

flowchart LR
  Settings["External Agent Settings"] -->|"persist executable"| Policy["RuntimePolicy"]
  Policy --> Coordinator["Builtin External-Agent Plugin Coordinator"]
  Bundles["release-shipped plugin.mjs bundles"] --> Coordinator
  Coordinator -->|"content-addressed install / replace / remove"| RuntimePkg["system package: acp-executor"]
  Coordinator -->|"configured child Entry"| AdapterPkg["system package: antigravity-acp"]
  RuntimePkg --> RuntimeEntry["isolated acp-runtime Entry"]
  RuntimeEntry --> AdapterEntry["antigravity-acp Entry<br/>executable config"]
  AdapterEntry --> Registry["PluginExecutorService inspection"]
Loading

RuntimePolicy remains the durable owner of setup facts. The coordinator creates replaceable derived Plugin packages in dependency order and compares canonical content digests before installation, so restart and repeated reconciliation do not churn Plugin generations. Clearing the setting removes the adapter first and then the shared runtime. Neither Plugin bundle receives RuntimePolicy authority.

Plugin composition and ownership

flowchart TB
  Profile["Profile Plugin root"]
  RuntimeEntry["acp-runtime Entry<br/>package: acp-executor"]
  Service["AcpRuntimeService<br/>provides ctx.acp"]
  AdapterEntry["antigravity-acp Entry<br/>inject: acp"]
  Adapter["AntigravityAcpAdapter"]
  Provider["registered PluginExecutorProvider"]

  Profile --> RuntimeEntry
  RuntimeEntry -->|"owns service lifetime"| Service
  RuntimeEntry -->|"parent Context"| AdapterEntry
  AdapterEntry -->|"inherits ctx.acp"| Service
  AdapterEntry --> Adapter
  Adapter -->|"register(ctx, adapter, config)"| Service
  Service -->|"ctx.executors.register"| Provider
Loading

The Host installs acp-executor as a system-managed package and contributes an isolated acp-runtime profile Entry. External-Agent Entries are mounted below it, so service availability and disposal follow normal Plugin Context/Fiber ownership. The adapter passes its consuming Context explicitly across independently bundled generations; executor registration remains scoped, transactional, generation-pinned, and retired by #5283's existing machinery.

One request lifecycle

sequenceDiagram
  participant User
  participant Session as Maka Session
  participant Backend as PluginExecutorBackend
  participant Service as PluginExecutorService
  participant ACP as ACP Runtime
  participant Agent as External ACP Agent

  User->>Session: send(turn, executorId)
  Session->>Backend: BackendSendInput
  Backend->>Service: execute(binding, request)
  Service->>ACP: execute(request, context)

  alt first prompt for this conversation
    ACP->>Agent: spawn + initialize
    ACP->>Agent: session/new(cwd)
    ACP->>Agent: setConfigOption (optional)
    ACP->>ACP: persist continuity marker
  end

  ACP->>Agent: session/prompt
  Agent-->>ACP: text / thought / tool updates
  ACP-->>Backend: generic output events
  Backend-->>Session: canonical SessionEvents

  opt Agent requests permission
    Agent->>ACP: session/requestPermission
    ACP->>Backend: context.requestPermission
    Backend->>User: Hosted Form
    User-->>Backend: selected / cancelled
    Backend-->>ACP: validated result
    ACP-->>Agent: ACP permission outcome
  end

  Agent-->>ACP: stopReason
  ACP-->>Service: completed / cancelled / failed
  Service-->>Backend: normalized terminal result
  Backend-->>Session: complete / abort / error
Loading

Cancellation flows in the reverse direction through the same chain: Session stop aborts the bound executor call, ACP sends session/cancel, waits for settlement, and force-terminates the process tree only when cooperative cleanup does not finish. Plugin disable/reload/uninstall uses the same retirement and drain path.

Responsibility boundary

Layer Owns Must not own
Maka Runtime executorId routing, scoped registration, generation binding, canonical events, Hosted Forms ACP Session ids, ACP processes, provider-specific models
ACP Runtime Plugin ACP SDK/stdio, process tree, per-conversation ACP Session, file callbacks, cancellation, common config and event projection Desktop selection state, Maka Session routing, Antigravity-specific environment policy
Agent adapter Plugin executor identity, executable/sidecars, arguments/environment, provider-specific config mapping ACP protocol loop, duplicated permission/file/tool plumbing
External Agent model execution and ACP protocol responses Maka persistence and Plugin lifecycle

The Antigravity adapter build is about 2.4 KB; the ACP SDK and shared lifecycle implementation live only in the runtime package. A future ACP provider implements the adapter contract instead of copying process, protocol, file, permission, cancellation, and event-projection code.

Removed from the previous implementation

The rebuild deliberately does not carry forward the old PR's:

  • Host-owned AcpAgentBackend and ACP backend registry;
  • backend: "acp" / externalAgentId Session and Storage branches;
  • external-Agent catalog and Session protocols;
  • renderer hot catalog cache and provider-specific task-entry state;
  • draft/prewarm ACP Session lease;
  • ACP-specific CLI transcript and model-picker forks;
  • WorkHub visual workaround and unrelated E2E coverage.

Those paths either duplicate #5283 or require a future generic executor configuration/selection capability. The installation and authentication foundation already merged in #5164 remains unchanged because it is still live mainline behavior.

Behavior and safety

  • ACP initialize, session/new, prompt, cancellation, and cleanup are shared across adapters.
  • ACP filesystem callbacks reject paths outside the Session workspace, including symlink escapes.
  • Required executables are resolved and checked before process launch.
  • Permission options cross a validated generic executor contract and settle through Maka's Hosted Form authority.
  • ACP diff content is preserved as canonical file_diff tool results; oversized diffs degrade to a bounded summary.
  • Plugin disable, reload, or uninstall aborts active work, drains executor calls, and terminates owned process trees.
  • Historical external conversations never silently continue with a newly created ACP Session after process continuity is lost.

Remaining PR 2 work

PR 2 remains one vertical PR. Work is tracked as four producer-to-consumer minimum sets inside this same PR:

  • Set A — setup facts → active Plugin executor: ship both production bundles, project the PR 1 executable into content-addressed system-managed packages/Entries, and verify real bundle install, idempotent restart/reconcile, config replacement, setting clear, package removal, and executor inspection.
  • Set B — provider catalog → Desktop choice → first ACP prompt: add generic readiness/catalog/configuration inspection (including unavailable/auth-required), Antigravity discovery, existing-menu integration, atomic Session executor/model persistence, and exact first-prompt application without preview Sessions.
  • Set C — ACP interactions → canonical conversation → ACP settlement: add Agent questions, actionable unsupported-input presentation, and ordered interaction/rendering regressions.
  • Set D — process continuity facts → safe task readiness: project history-only/process loss into generic Desktop readiness and repeat controlled-process plus official Antigravity acceptance.

A checklist group is complete only when its producer, boundary contract, real consumer, and acceptance test land together. Dynamic modes and catalog invalidation remain PR 4; restoring the same external Session remains PR 3.

Verification

Current head: 169b969df

Check Result
Workspace TypeScript typecheck Passed
Workspace build, including Desktop renderer Passed
Changed-file Biome checks Passed
Clean Runtime Host suite 1,938 passed, 12 skipped, 0 failed
ACP/Plugin/runtime-policy focused suite 80/80 passed
Desktop architecture suite 112/112 passed
Release/product/Desktop suite 72/72 passed
Production plugin.mjs install/restart/config-clear test Passed
Both Plugin builds and package-content checks Passed
git diff --check and staged ASF header guard Passed

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: OpenAI Codex contributed architecture analysis, implementation, tests, verification, documentation, and PR preparation. Independent human review remains required.

Checklist

  • Rebuilt on the latest main
  • Removed the superseded ACP-specific architecture
  • Tests cover lifecycle, continuity, cancellation, permission bridging, diff projection, and adapter registration
  • Lint, typecheck, builds, and affected suites pass locally

@Sun-GLiang
Sun-GLiang force-pushed the feat/antigravity-acp-pr2 branch from 9767546 to b278ff0 Compare September 14, 2026 14:09
@Sun-GLiang Sun-GLiang changed the title feat(runtime-host): add ACP connection ownership and sign-in state feat(runtime): rebuild ACP execution as plugin adapters Sep 14, 2026
@github-actions github-actions Bot added effort/XL Under 2500 readable lines and removed effort/XXL Over 2500 readable lines labels Sep 15, 2026

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — comment only

Targeted review of the ACP rebuild. I did not read all ~2.5k lines; I focused on packages/acp-executor-plugin/src/index.ts, packages/antigravity-acp-plugin/src/index.ts, packages/runtime-host/src/server/builtin-external-agent-plugins.ts, the generic boundary changes in packages/runtime/src/plugin-executor-{service,backend}.ts, the host wiring, and the tests.

The boundary design reads well and I think it's the right shape: ACP mechanics stay in one bundle, adapters own only launch policy, and routing/lifecycle stay with #5283's generic executor path. The items below are about robustness and one class of silent failure, not about the architecture.

Issues

1. AcpExecutor marks a conversation permanently "history-only" for every execution failure, not just process losspackages/acp-executor-plugin/src/index.ts:212 and :215

execute()'s catch calls await this.#lose(session) unconditionally, and #lose (:508) sets session.lost = true, which makes #session() throw acp_history_only for that conversationKey for the rest of the Entry generation. That's correct for genuine process loss (owner.failed, cancel timeout at :499), but it also fires for:

  • the 30s INITIALIZE_TIMEOUT_MS (:59, :283) — a slow first spawn bricks the conversation;
  • acp_config_unavailable / acp_config_invalid from #applyInitialConfig — a wrong model value in the Entry config permanently disables the conversation even after the config is corrected;
  • a transient checkedExecutable ENOENT while the agent app is being reinstalled;
  • a caller abort during #ensureInitialized#initialize uses AbortSignal.any([signal, timeout]) (:283), so startupSignal.throwIfAborted() on a user stop during the first prompt also lands here.

The follow-up error is also misleading: it reports "history-only because its external process was lost" when no process was ever started. Suggest only calling #lose() when the connection actually failed, and for other errors clearing session.initialization/session.owner so the next prompt can retry.

2. Agent-authored tool metadata and >64 diffs throw inside context.emit, and the ACP SDK swallows the errorpackages/acp-executor-plugin/src/index.ts:421-470, packages/runtime/src/plugin-executor-service.ts:507

PluginExecutorService's emit wrapper calls normalizeOutputEvent outside its try/catch (plugin-executor-service.ts:296-306), so a validation failure throws back into #acceptTool, which runs inside the ACP session/update notification handler. The SDK's dispatch catches notification-handler errors and only console.error("Error handling notification", …) — the connection stays up, so the failure is silent in the product. Two realistic triggers:

  • #acceptTool passes displayName: snapshot.title and name: snapshot.name ?? … straight from the agent. normalizeOutputEvent bounds displayName at 8192 chars and name at 256 chars with no \r\n (plugin-executor-service.ts:423-426). A long agent-authored title makes tool_start throw, and because toolUseIds is only populated on a successful tool_start (plugin-executor-backend.ts #publishOutputEvent), the later tool_result is dropped too and #closeOptionalOutput never backstops it — the tool vanishes from the transcript entirely.
  • projectToolResult (:781) bounds the combined diff at MAX_TOOL_RESULT_DIFF but not the path count, while isPluginToolResultContent rejects paths.length > 64 (plugin-executor-service.ts:507). One multi-file tool call (a rename across >64 files) makes the tool_result emit throw; snapshot.terminal = true is set before the emit (:457), so no later tool_call_update retries it. The backend then backstops with the synthetic "External executor ended before reporting a tool result" for a tool that actually succeeded.

Suggest clamping displayName/name/paths before crossing the boundary (or degrading >64 paths to the text summary), setting terminal only after a successful emit, and wrapping #acceptUpdate in a try/catch that logs.

3. Retained ACP processes are unboundedpackages/acp-executor-plugin/src/index.ts:149, :255, :226

#sessions only ever grows (set in #session, cleared only in dispose), and every entry keeps a live child process plus its tree. One retained process per Maka conversation key, for the lifetime of the Entry generation, with no idle eviction, cap, or LRU. A long-running Host will accumulate one Antigravity process per conversation ever started. Either bound it (evict idle sessions — the continuity marker already encodes the "history-only" consequence) or state the limit explicitly in the README so operators know.

4. No authentication handling on the execution pathpackages/acp-executor-plugin/src/index.ts:579

child.stderr is drained to nowhere and initializeResponse.authMethods is ignored (the SDK exposes it at dist/schema/zod.gen.js:1134, along with an authenticate method). The setup path merged in #5164 does parse the auth line out of stderr (packages/runtime-host/src/server/acp/antigravity.ts:144-160). If the saved Antigravity login expires, the first prompt fails as acp_execution_failed / "ACP execution failed" with no re-auth affordance and nothing logged. At minimum, surface authMethods as a distinct code so the Desktop can route back into the existing setup flow.

5. A plugin-projection failure requests a Host drainpackages/runtime-host/src/server/execution-composition.ts:1962-1967

applyRuntimePolicyMutationEffects now runs builtinExternalAgentPlugins.reconcile() inside the existing context.requestDrain(); throw error; path. The setting is already committed by then, and HostPluginPlatform already records the failure and schedules its own reconcile (#recordPackageFailure / #scheduleReconcile), so draining the whole Host because a derived, replaceable package layer failed to install seems heavier than needed. Worth confirming this is intended.

Nits

  • Dropped SessionUpdate kinds#acceptUpdate (:405) handles 4 of the 14 kinds in the SDK union. config_option_update is dropped, so the in-memory session.configOptions goes stale once the agent changes a value mid-session (relevant to Set B); plan/plan_update are dropped as well. A debug log for dropped/unknown kinds would make agent behavior diagnosable.
  • Diagnostics are discardederrorCode (:879) collapses every non-AcpRuntimeError to acp_execution_failed and safeErrorMessage (:886) to 'ACP execution failed', with no cause and no log line. This makes issues 1 and 4 very hard to diagnose in the field.
  • String-sniffed error channelactive.text.trimStart().startsWith('Agent execution error:') (:200) treats model-authored transcript text as a failure signal. A response that legitimately begins with that phrase fails the turn. Prefer an explicit stop reason / _meta signal if Antigravity exposes one.
  • supportsAttachments is static — the initialize response already reports agentCapabilities.promptCapabilities (image/audio/embeddedContext). Deriving support from the negotiated capabilities would avoid failing a whole turn with acp_attachments_unsupported for agents that do accept images.
  • Prompt flatteningpromptText folds instructions, quotes, and directory references into prose prefixes inside one text block. ACP ContentBlock supports resource_link/resource/image, which would carry that structure instead of text the agent may act on. Relatedly, session/new sends mcpServers: [], so ACP agents get none of Maka's MCP servers — worth stating that explicitly.
  • dispose() throwing breaks teardownterminate() throws 'ACP process cleanup failed' if the child is still alive at the deadline, and dispose() aggregates that, so a stuck child makes the fiber's effect cleanup fail during plugin uninstall/reload. Consider logging and continuing. (terminate also has no final close/exit await, so a child exiting just past the last poll is a false positive.)
  • createWholeFileDiff (:812) emits a single whole-file hunk with no \ No newline at end of file marker, so a diff whose oldText/newText lacks a trailing newline is technically malformed for strict patch consumers.
  • Bundled host runtime codepackage.json lists @maka/runtime as a devDependency, but index.ts value-imports terminateChildProcessTree from @maka/runtime/process-tree-terminator, so that implementation is esbuild-bundled into dist/plugin.mjs. That's consistent with the deliberate "no cross-bundle instanceof" isolation, but it means the shipped bundle carries its own copy that won't track @maka/runtime. Worth a line in the README.
  • Docs self-referencedocs/antigravity-acp-plugin-rebuild.md opens with "Why PR #5224 cannot be carried forward unchanged" / "PR #5224 predates #5283", but this is PR #5224, so the doc reads as arguing against itself. Naming it "the pre-#5283 revision of this PR" would fix it.
  • Non-hosted permission denialPluginExecutorBackend.#requestPermission (plugin-executor-backend.ts:186) returns { outcome: 'cancelled' } whenever input.hostedInteraction is absent, so every ACP permission request is denied for CLI/API/scheduled execution. Safe default, but worth documenting since it silently narrows what external agents can do headlessly.

Verified / no action needed

  • Recovery ordering is correct. builtin-external-agent-plugins is registered after plugin-platform in the module array (execution-composition.ts:2504, platform module ~:2494), and recoverRuntimeHostDomainModules iterates in array order, so recover()'s packageProjections()/installPackage() hit a platform where #assertReadable() passes. Good — I checked this specifically because the coordinator depends on platform recovery.
  • Digest idempotency holds. extensionPackageDirectoryContentDigest uses the same sorted-path + content hash as PluginPackageStore.decodePackage, and prepareInstall copies into its own transaction directory before the staging dir is disposed, so the "no generation churn on restart" claim is sound (and builtin-external-agent-plugins.test.ts asserts a stable authorityEpoch).
  • Packaging. dependencies is in WORKSPACE_RELEASE_MANIFEST_FIELDS, so @maka/acp-executor-plugin / @maka/antigravity-acp-plugin reach the packaged app through runtime-host's production closure (files: ["dist"] includes plugin.mjs).
  • Adapter isolation. isolate: { acp: true } gives the acp-runtime Entry its own acp label that children inherit; ctx.provide('acp', …) is on the runtime Entry's Context and the adapter passes its own Context explicitly, so executors.register scopes to the consumer Entry (PluginExecutorService.register reads this.ctx, which Service._bind rebinds per consumer).
  • Cancellation and teardown. #awaitPrompt sends session/cancel, waits for settlement, and force-terminates only on the 15s timeout; PluginExecutorService's retirement path aborts active executions and awaits settlement, and the effect cleanup disposes the provider (process trees included).
  • Antigravity launch policy matches the live setup path in packages/runtime-host/src/server/acp/antigravity.ts (BROWSER=/usr/bin/true, PYTHONUNBUFFERED=1, ANTIGRAVITY_HARNESS_PATH, cwd = dirname(executable), localharness_external helper precheck).
  • Generic boundary changes are additive and validated symmetricallyPluginExecutorToolResultContent is a bounded discriminated union, file_diff already exists in the canonical ToolResultEvent shape, and normalizePermissionRequest/normalizePermissionResult validate both directions with the form withdrawn on abort.
  • Concurrency. One prompt per conversation is enforced by session.active (acp_busy), and session.initialization de-dupes concurrent initialization.

Test coverage vs. the PR checklist

The checklist says tests cover "lifecycle, continuity, cancellation, permission bridging, diff projection, and adapter registration". acp-executor-plugin.test.ts has 3 tests (retention, history-only, cancellation); permission bridging and file_diff are asserted only incidentally inside the first. The "Behavior and safety" claims with no test: workspace containment including symlink escape for fs/readTextFile / fs/writeTextFile, the 8 MiB file cap, the diff-size degrade path, setConfigOption validation, adapter/config validation, process-tree termination on dispose, and the durable pluginStateStore (only a fake store is exercised). Containment and the diff-degrade path are the two I'd add first, since they're the security/robustness claims.

Coordination

#5283 is the merged base and #5222/#5385/#5386 are CLI-side ACP work that doesn't touch these packages, so I don't see a conflict. The one seam worth aligning is mcpServers: [] above: #5386 adds session-scoped ACP MCP on the CLI side, and the runtime plugin currently opts out entirely. Similarly, this PR's "Set C" (agent questions, unsupported-input presentation) overlaps #5385's interaction mapping — worth a quick sync so the two don't land incompatible contracts.

@Sun-GLiang
Sun-GLiang marked this pull request as draft September 17, 2026 12:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/XL Under 2500 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants